package mirrormonkey.core.server;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;

import mirrormonkey.core.EntityLifecycleListener;
import mirrormonkey.core.member.SearchCompatibleKey;
import mirrormonkey.core.member.SearchExactKey;
import mirrormonkey.core.member.SearchKey;
import mirrormonkey.core.member.SearchNamedKey;
import mirrormonkey.core.member.SearchNamedUnsafeKey;
import mirrormonkey.framework.ConnectionInfo;
import mirrormonkey.framework.entity.EntityData;
import mirrormonkey.framework.entity.StaticEntityData;
import mirrormonkey.framework.entity.SyncEntity;
import mirrormonkey.framework.member.DynamicMemberData;

import com.jme3.network.MessageConnection;

public class ServerEntityData extends EntityData {

	private static final Collection<ConnectionInfo> NO_CONNECTIONS = Collections
			.unmodifiableCollection(new LinkedList<ConnectionInfo>());

	private static final boolean DEFAULT_OVERRIDE_OPTION = false;

	private final ServerCoreModule module;

	private final Map<Class<?>, Interpretation> memberData;

	private final Map<MessageConnection, ExistingMappingStack> connections;

	public ServerEntityData(ServerCoreModule module, int id, SyncEntity entity) {
		super(id, entity);
		this.module = module;
		memberData = new HashMap<Class<?>, Interpretation>();
		connections = new HashMap<MessageConnection, ExistingMappingStack>();
	}

	@Override
	public StaticEntityData getStaticData(MessageConnection forConnection) {
		ExistingMappingStack stack = connections.get(forConnection);
		return stack == null ? null : stack.getTop().getStaticData();
	}

	public StaticEntityData getStaticData(
			Class<? extends SyncEntity> connectedClass) {
		return memberData.get(connectedClass).getStaticData();
	}

	@Override
	public DynamicMemberData[] getMemberData(StaticEntityData staticData) {
		Interpretation i = memberData.get(staticData.connectedClass);
		return i == null ? null : i.interpretedMemberData;
	}

	@Override
	public Collection<? extends ConnectionInfo> getConnections(
			StaticEntityData staticData) {
		Interpretation i = memberData.get(staticData.connectedClass);
		return i == null ? NO_CONNECTIONS : i.getConnections();
	}

	public ExistingMappingStack getStack(MessageConnection connection) {
		return connections.get(connection);
	}

	@Override
	public void destroy() {
		module.getEntityProvider().assertNotOutdated(this);
		while (!connections.isEmpty()) {
			connections.values().iterator().next().destroy();
		}
		registered = false;
		if (entity != null) {
			entity.setData(null);
		}
		this.getListenerConfiguration().clear();
		module.getEntityProvider().removeData(this);
	}

	/*
	 * callConstr main entry point. The following methods {call*Constr} are
	 * shortcuts to this method to hide the SearchKey class from users that
	 * don't implement their own keys
	 */
	public MappingStackElement callConstr(boolean override,
			Class<? extends SyncEntity> connectedClass, SearchKey search) {
		module.getEntityProvider().assertNotOutdated(this);
		return module.getStack(getEntity(), this, search.getConnection())
				.find(connectedClass).addRef(override, search);
	}

	/*
	 * Exact class match for parameters
	 */

	public MappingStackElement callConstr(MessageConnection connection,
			Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, getEntity().getClass(),
				new SearchExactKey(connection, constrParams));
	}

	public MappingStackElement callConstr(MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, connectedClass,
				new SearchExactKey(connection, constrParams));
	}

	public MappingStackElement callConstr(boolean override,
			MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, Object... constrParams) {
		return callConstr(override, connection, connectedClass,
				new SearchExactKey(connection, constrParams));
	}

	/*
	 * Search for constructor with @NamedConstructor
	 */

	public MappingStackElement callNamedConstr(MessageConnection connection,
			String constrName, Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, getEntity().getClass(),
				new SearchNamedKey(connection, constrParams, constrName));
	}

	public MappingStackElement callNamedConstr(MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, String constrName,
			Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, connectedClass,
				new SearchNamedKey(connection, constrParams, constrName));
	}

	public MappingStackElement callNamedConstr(boolean override,
			MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, String constrName,
			Object... constrParams) {
		return callConstr(override, connectedClass, new SearchNamedKey(
				connection, constrParams, constrName));
	}

	/*
	 * Search for a named constructor BUT DO NOT CHECK ON THE SERVER SIDE IF THE
	 * PARAMETERS ARE ASSIGNMENT-COMPATIBLE
	 */

	public MappingStackElement callUnsafeConstr(MessageConnection connection,
			String constrName, Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, getEntity().getClass(),
				new SearchNamedUnsafeKey(connection, constrParams, constrName));
	}

	public MappingStackElement callUnsafeConstr(MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, String constrName,
			Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, connectedClass,
				new SearchNamedUnsafeKey(connection, constrParams, constrName));
	}

	public MappingStackElement callUnsafeConstr(boolean override,
			MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, String constrName,
			Object... constrParams) {
		return callConstr(override, connectedClass, new SearchNamedUnsafeKey(
				connection, constrParams, constrName));
	}

	/*
	 * Search for ANY constructor that can be called with the given parameters
	 * (IF THERE ARE MULTIPLE CONSTRUCTORS THAT DO, THE CONSTRUCTOR THAT WILL BE
	 * CALLED MAY BE DIFFERENT FOR EACH CALL)
	 */
	public MappingStackElement callAnyConstr(MessageConnection connection,
			Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, getEntity().getClass(),
				new SearchCompatibleKey(connection, constrParams));
	}

	public MappingStackElement callAnyConstr(MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, Object... constrParams) {
		return callConstr(DEFAULT_OVERRIDE_OPTION, connectedClass,
				new SearchCompatibleKey(connection, constrParams));
	}

	public MappingStackElement callAnyConstr(boolean override,
			MessageConnection connection,
			Class<? extends SyncEntity> connectedClass, Object... constrParams) {
		return callConstr(override, connectedClass, new SearchCompatibleKey(
				connection, constrParams));
	}

	public MappingStackElement remove(MessageConnection connection) {
		module.getEntityProvider().assertNotOutdated(this);
		return module.getStack(getEntity(), this, connection).getTop()
				.removeRef();
	}

	public MappingStackElement remove(MessageConnection connection,
			Class<? extends SyncEntity> connectedClass) {
		module.getEntityProvider().assertNotOutdated(this);
		return module.getStack(getEntity(), this, connection)
				.find(connectedClass).removeRef();
	}

	private Interpretation getOrCreateInterpretation(MappingStackElement e) {
		Interpretation i = memberData.get(e.getStaticData().connectedClass);
		if (i == null) {
			i = new Interpretation(e.getStaticData());
		}
		return i;
	}

	protected void checkRegister() {
		if (!isRegistered()) {
			entity.setData(this);
			module.getEntityProvider().registerData(this);
			registered = true;
		}
	}

	protected void checkDestroy() {
		if (isRegistered() && getListenerConfiguration().isEmpty()
				&& connections.isEmpty()) {
			destroy();
		}
	}

	protected void addMapping(ServerCoreModule module,
			ServerConnectionInfo connectionInfo, MappingStackElement newElement) {
		Interpretation i = getOrCreateInterpretation(newElement);
		connections.put(connectionInfo.getConnection(),
				(ExistingMappingStack) newElement.stack);
		i.addConnection(connectionInfo);
		checkRegister();
	}

	protected void changeMapping(ServerConnectionInfo connectionInfo,
			MappingStackElement oldElement, MappingStackElement newElement) {
		System.out.println("CHANGE MAPPING:\n  " + oldElement + "\n  "
				+ newElement + "\n  " + memberData);
		Interpretation i = memberData
				.get(oldElement.getStaticData().connectedClass);
		i.removeConnection(connectionInfo);
		i = getOrCreateInterpretation(newElement);
		i.addConnection(connectionInfo);
	}

	protected void removeMapping(ServerConnectionInfo connectionInfo,
			MappingStackElement lastElement) {
		// called with correct last element (at least in the first run), but
		// memberData contains old
		// interpretation
		System.out.println(getClass().getName()
				+ ".removeMapping() before:\n  "
				+ connectionInfo.getConnection() + "\n  " + connections
				+ "\n  " + lastElement + "\n  " + memberData);
		Interpretation i = memberData
				.get(lastElement.getStaticData().connectedClass);
		if (i != null) {
			System.out.println("attempt remove");
			i.removeConnection(connectionInfo);
			connections.remove(connectionInfo.getConnection());
		}
		System.out.println(getClass().getName() + ".removeMapping() after:\n  "
				+ connections);
		checkDestroy();
	}

	@Override
	public void addEntityLifecycleListener(EntityLifecycleListener listener) {
		module.getEntityProvider().assertNotOutdated(this);
		checkRegister();
		super.addEntityLifecycleListener(listener);
	}

	@Override
	public void removeEntityLifecycleListener(EntityLifecycleListener listener) {
		module.getEntityProvider().assertNotOutdated(this);
		super.removeEntityLifecycleListener(listener);
		checkDestroy();
	}

	private final class Interpretation {

		private final StaticEntityData staticData;

		private final DynamicMemberData[] interpretedMemberData;

		private final Collection<ServerConnectionInfo> connections;

		public Interpretation(StaticEntityData staticData) {
			this.staticData = staticData;
			interpretedMemberData = staticData.createDynamicMemberData();
			connections = new LinkedHashSet<ServerConnectionInfo>();
		}

		public final Collection<ServerConnectionInfo> getConnections() {
			return connections;
		}

		public final void addConnection(ServerConnectionInfo info) {
			boolean emptyBefore = connections.isEmpty();
			connections.add(info);
			if (emptyBefore) {
				getStaticData().addEntity(ServerEntityData.this);
				memberData.put(getStaticData().connectedClass, this);
			}
		}

		public final void removeConnection(ServerConnectionInfo info) {
			boolean emptyBefore = connections.isEmpty();
			connections.remove(info);
			if (!emptyBefore && connections.isEmpty()) {
				getStaticData().removeEntity(ServerEntityData.this);
				memberData.remove(getStaticData().connectedClass);
			}
		}

		public StaticEntityData getStaticData() {
			return staticData;
		}

	}
}
